/**
 * Copyright (C) 2021-2022 maminjie <canpool@163.com>
 * SPDX-License-Identifier: LGPL-2.1
 * Powered by Qt-Advanced-Docking-System
**/

#include "dockwidget.h"

#include "dockmanager_p.h"
#include "dockpannel.h"
#include "dockcontainer.h"
#include "dockmanager.h"
#include "docksplitter.h"
#include "docktab.h"
#include "dockfloatingwidget.h"

#include <QAction>
#include <QBoxLayout>
#include <QEvent>
#include <QLoggingCategory>
#include <QPointer>
#include <QScrollArea>
#include <QSplitter>
#include <QStack>
#include <QTextStream>
#include <QToolBar>
#include <QXmlStreamWriter>
#include <QWindow>

QADS_BEGIN_NAMESPACE

class DockWidgetPrivate
{
public:
    struct WidgetFactory {
        DockWidget::FactoryFunc createWidget;
        DockWidget::InsertMode insertMode;
    };

public:
    DockWidgetPrivate(DockWidget *parent);

    void showDockWidget();
    void hideDockWidget();

    void updateParentDockPannel();

    void setupToolBar();
    void setupScrollArea();

    void setToolBarFloatingStyle(bool floating);

    /**
     * Creates the content widget with the registered widget factory and returns true on success.
     */
    bool createWidgetFromFactory();

public:
    DockWidget *q = nullptr;
    QBoxLayout *m_layout = nullptr;
    QWidget *m_widget = nullptr;
    DockTab *m_tab = nullptr;
    DockWidget::DockWidgetFeatures m_features = DockWidget::DefaultDockWidgetFeatures;
    DockManager *m_dockManager = nullptr;
    DockPannel *m_dockPannel = nullptr;
    QAction *m_toggleViewAction = nullptr;
    bool m_closed = false;
    QScrollArea *m_scrollArea = nullptr;
    QToolBar *m_toolBar = nullptr;
    Qt::ToolButtonStyle m_toolBarButtonStyleDocked = Qt::ToolButtonIconOnly;
    Qt::ToolButtonStyle m_toolBarButtonStyleFloating = Qt::ToolButtonTextUnderIcon;
    QSize m_toolBarIconSizeDocked = QSize(16, 16);
    QSize m_toolBarIconSizeFloating = QSize(24, 24);
    bool m_isFloatingTopLevel = false;
    QList<QAction *> m_titleBarActions;
    DockWidget::MinimumSizeHintMode m_minimumSizeHintMode = DockWidget::MinimumSizeHintFromDockWidget;
    WidgetFactory *m_factory = nullptr;
}; // class DockWidgetPrivate

DockWidgetPrivate::DockWidgetPrivate(DockWidget *parent)
    : q(parent)
{}

void DockWidgetPrivate::showDockWidget()
{
    if (!m_widget) {
        if(!createWidgetFromFactory()) {
            Q_ASSERT(!m_features.testFlag(DockWidget::DeleteContentOnClose)
                        && "DeleteContentOnClose flag was set, but the widget "
                           "factory is missing or it doesn't return a valid QWidget.");
            return;
        }
    }
    if (!m_dockPannel) {
        DockFloatingWidget *floatingWidget = new DockFloatingWidget(q);
        // We use the size hint of the content widget to provide a good initial size
        floatingWidget->resize(m_widget ? m_widget->sizeHint() : q->size());
        m_tab->show();
        floatingWidget->show();
    } else {
        m_dockPannel->setCurrentDockWidget(q);
        m_dockPannel->toggleView(true);
        m_tab->show();
        QSplitter *splitter = internal::findParent<QSplitter *>(m_dockPannel);
        while (splitter && !splitter->isVisible()) {
            splitter->show();
            splitter = internal::findParent<QSplitter *>(splitter);
        }

        DockContainer *container = m_dockPannel->dockContainer();
        if (container->isFloating()) {
            DockFloatingWidget *floatingWidget
                    = internal::findParent<DockFloatingWidget *>(container);
            floatingWidget->show();
        }
    }
}

void DockWidgetPrivate::hideDockWidget()
{
    m_tab->hide();
    updateParentDockPannel();
    if (m_features.testFlag(DockWidget::DeleteContentOnClose)) {
        m_widget->deleteLater();
        m_widget = nullptr;
    }
}

/**
 * Hides a dock pannel if all dock widgets in the pannel are closed.
 * This function updates the current selected tab and hides the parent
 * dock pannel if it is empty
 */
void DockWidgetPrivate::updateParentDockPannel()
{
    if (!m_dockPannel) {
        return;
    }

    // we don't need to change the current tab if the current DockWidget is not the one being closed
    if (m_dockPannel->currentDockWidget() != q) {
        return;
    }

    auto nextDockWidget = m_dockPannel->nextOpenDockWidget(q);
    if (nextDockWidget) {
        m_dockPannel->setCurrentDockWidget(nextDockWidget);
    } else {
        m_dockPannel->hideWithNoVisibleContent();
    }
}

void DockWidgetPrivate::setupToolBar()
{
    m_toolBar = new QToolBar(q);
    m_toolBar->setObjectName("dockWidgetToolBar");
    m_layout->insertWidget(0, m_toolBar);
    m_toolBar->setIconSize(QSize(16, 16));
    m_toolBar->toggleViewAction()->setEnabled(false);
    m_toolBar->toggleViewAction()->setVisible(false);
}

void DockWidgetPrivate::setupScrollArea()
{
    m_scrollArea = new QScrollArea(q);
    m_scrollArea->setObjectName("dockWidgetScrollArea");
    m_scrollArea->setWidgetResizable(true);
    m_layout->addWidget(m_scrollArea);
}

void DockWidgetPrivate::setToolBarFloatingStyle(bool floating)
{
    if (!m_toolBar) {
        return;
    }

    auto iconSize = floating ? m_toolBarIconSizeFloating : m_toolBarIconSizeDocked;
    if (iconSize != m_toolBar->iconSize()) {
        m_toolBar->setIconSize(iconSize);
    }

    auto buttonStyle = floating ? m_toolBarButtonStyleFloating : m_toolBarButtonStyleDocked;
    if (buttonStyle != m_toolBar->toolButtonStyle()) {
        m_toolBar->setToolButtonStyle(buttonStyle);
    }
}

bool DockWidgetPrivate::createWidgetFromFactory()
{
    if (!m_features.testFlag(DockWidget::DeleteContentOnClose)) {
        return false;
    }
    if (!m_factory) {
        return false;
    }
    QWidget *w = m_factory->createWidget(q);
    if (!w) {
        return false;
    }
    q->setWidget(w, m_factory->insertMode);
    return true;
}

DockWidget::DockWidget(const QString &uniqueId, QWidget *parent)
    : QWidget(parent)
    , d(new DockWidgetPrivate(this))
{
    setAttribute(Qt::WA_StyledBackground, true);
    d->m_layout = new QBoxLayout(QBoxLayout::TopToBottom);
    d->m_layout->setContentsMargins(0, 0, 0, 0);
    d->m_layout->setSpacing(0);
    setLayout(d->m_layout);
    setWindowTitle(uniqueId); // temporarily use unique id as title
    setObjectName(uniqueId);
    d->m_tab = new DockTab(this);
    d->m_toggleViewAction = new QAction(uniqueId, this);
    d->m_toggleViewAction->setCheckable(true);
    connect(d->m_toggleViewAction, &QAction::triggered, this, &DockWidget::toggleView);
    d->setToolBarFloatingStyle(false);

    if (QAds::testConfigFlag(QAds::FocusHighlighting)) {
        setFocusPolicy(Qt::ClickFocus);
    }
}

DockWidget::~DockWidget()
{
    delete d;
}

void DockWidget::setWidget(QWidget *widget, InsertMode mode)
{
    if (d->m_widget) {
        takeWidget();
    }

    auto scrollAreaWidget = qobject_cast<QAbstractScrollArea *>(widget);
    if (scrollAreaWidget || ForceNoScrollArea == mode) {
        d->m_layout->addWidget(widget);

        if (scrollAreaWidget && scrollAreaWidget->viewport()) {
            scrollAreaWidget->viewport()->setProperty("dockWidgetContent", true);
        }
    } else {
        d->setupScrollArea();
        d->m_scrollArea->setWidget(widget);
    }

    d->m_widget = widget;
    d->m_widget->setProperty("dockWidgetContent", true);
}

QWidget *DockWidget::widget() const
{
    return d->m_widget;
}

QWidget *DockWidget::takeWidget()
{
    QWidget *w = nullptr;

    if (d->m_scrollArea) {
        d->m_layout->removeWidget(d->m_scrollArea);
        w = d->m_scrollArea->takeWidget();
        delete d->m_scrollArea;
        d->m_scrollArea = nullptr;
        d->m_widget = nullptr;
    } else if (d->m_widget) {
        d->m_layout->removeWidget(d->m_widget);
        w = d->m_widget;
        d->m_widget = nullptr;
    }
    if (w) {
        w->setParent(nullptr);
    }

    return w;
}

DockWidget::DockWidgetFeatures DockWidget::features() const
{
    return d->m_features;
}

void DockWidget::setFeatures(DockWidgetFeatures features)
{
    if (d->m_features == features) {
        return;
    }

    d->m_features = features;
    emit featuresChanged(d->m_features);
    d->m_tab->onDockWidgetFeaturesChanged();
}

void DockWidget::setFeature(DockWidgetFeature flag, bool on)
{
    auto currentFeatures = features();
    currentFeatures.setFlag(flag, on);
    setFeatures(currentFeatures);
}

DockManager *DockWidget::dockManager() const
{
    return d->m_dockManager;
}

DockContainer *DockWidget::dockContainer() const
{
    if (d->m_dockPannel) {
        return d->m_dockPannel->dockContainer();
    } else {
        return nullptr;
    }
}

DockPannel *DockWidget::dockPannel() const
{
    return d->m_dockPannel;
}

DockFloatingWidget* DockWidget::dockFloatingWidget() const
{
    auto DockContainer = dockContainer();
    return DockContainer ? DockContainer->floatingWidget() : nullptr;
}

DockTab *DockWidget::tab() const
{
    return d->m_tab;
}

QAction *DockWidget::toggleViewAction() const
{
    return d->m_toggleViewAction;
}

void DockWidget::setToggleViewActionMode(ToggleViewActionMode mode)
{
    if (ActionModeToggle == mode) {
        d->m_toggleViewAction->setCheckable(true);
        d->m_toggleViewAction->setIcon(QIcon());
    } else {
        d->m_toggleViewAction->setCheckable(false);
        d->m_toggleViewAction->setIcon(d->m_tab->icon());
    }
}

void DockWidget::setMinimumSizeHintMode(MinimumSizeHintMode mode)
{
    d->m_minimumSizeHintMode = mode;
}

QIcon DockWidget::icon() const
{
    return d->m_tab->icon();
}

void DockWidget::setIcon(const QIcon &icon)
{
    d->m_tab->setIcon(icon);

    if (!d->m_toggleViewAction->isCheckable()) {
        d->m_toggleViewAction->setIcon(icon);
    }
}

QToolBar *DockWidget::toolBar() const
{
    if (!d->m_toolBar) {
        d->setupToolBar();
    }

    return d->m_toolBar;
}

void DockWidget::setToolBar(QToolBar *toolBar)
{
    if (d->m_toolBar) {
        d->m_layout->removeWidget(d->m_toolBar);
        delete d->m_toolBar;
    }

    d->m_toolBar = toolBar;
    d->m_layout->insertWidget(0, d->m_toolBar);
    d->setToolBarFloatingStyle(isFloating());
}

Qt::ToolButtonStyle DockWidget::toolBarButtonStyle(State state) const
{
    if (StateFloating == state) {
        return d->m_toolBarButtonStyleFloating;
    } else {
        return d->m_toolBarButtonStyleDocked;
    }
}

void DockWidget::setToolBarButtonStyle(Qt::ToolButtonStyle style, State state)
{
    if (StateFloating == state) {
        d->m_toolBarButtonStyleFloating = style;
    } else {
        d->m_toolBarButtonStyleDocked = style;
    }

    d->setToolBarFloatingStyle(isFloating());
}

QSize DockWidget::toolBarIconSize(State state) const
{
    if (StateFloating == state) {
        return d->m_toolBarIconSizeFloating;
    } else {
        return d->m_toolBarIconSizeDocked;
    }
}

void DockWidget::setToolBarIconSize(const QSize &iconSize, State state)
{
    if (StateFloating == state) {
        d->m_toolBarIconSizeFloating = iconSize;
    } else {
        d->m_toolBarIconSizeDocked = iconSize;
    }

    d->setToolBarFloatingStyle(isFloating());
}

QList<QAction *> DockWidget::titleBarActions() const
{
    return d->m_titleBarActions;
}

void DockWidget::setTitleBarActions(QList<QAction *> actions)
{
    // actions will show in titlebar
    d->m_titleBarActions = actions;
}

#ifndef QT_NO_TOOLTIP
void DockWidget::setTabToolTip(const QString &text)
{
    if (d->m_tab) {
        d->m_tab->setToolTip(text);
    }

    if (d->m_toggleViewAction) {
        d->m_toggleViewAction->setToolTip(text);
    }

    if (d->m_dockPannel) {
        d->m_dockPannel->markTitleBarMenuOutdated();    // update tabs menu
    }
}
#endif

bool DockWidget::isCentralWidget() const
{
    return dockManager()->centralWidget() == this;
}

bool DockWidget::isCurrentTab() const
{
    return d->m_dockPannel && (d->m_dockPannel->currentDockWidget() == this);
}

void DockWidget::setAsCurrentTab()
{
    if (d->m_dockPannel && !isClosed()) {
        d->m_dockPannel->setCurrentDockWidget(this);
    }
}

bool DockWidget::isFloating() const
{
    if (!isInFloatingContainer()) {
        return false;
    }
    return dockContainer()->topLevelDockWidget() == this;
}

void DockWidget::setFloating()
{
    if (isClosed()) {
        return;
    }

    d->m_tab->detachDockWidget();
}

bool DockWidget::isInFloatingContainer() const
{
    auto container = dockContainer();
    if (!container) {
        return false;
    }
    if (!container->isFloating()) {
        return false;
    }
    return true;
}


bool DockWidget::isTabbed() const
{
    return d->m_dockPannel && (d->m_dockPannel->openDockWidgetsCount() > 1);
}

bool DockWidget::isFullScreen() const
{
    if (isFloating()) {
        return dockContainer()->floatingWidget()->isFullScreen();
    } else {
        return QWidget::isFullScreen();
    }
}
bool DockWidget::isClosed() const
{
    return d->m_closed;
}

void DockWidget::toggleView(bool open)
{
    // If the toggle view action mode is ActionModeShow, then Open is always
    // true if the sender is the toggle view action
    QAction *action = qobject_cast<QAction *>(sender());
    if (action == d->m_toggleViewAction && !d->m_toggleViewAction->isCheckable()) {
        open = true;
    }

    // If the dock widget state is different, then we really need to toggle
    // the state. If we are in the right state, then we simply make this
    // dock widget the current dock widget
    if (d->m_closed != !open) {
        toggleViewInternal(open);
    } else if (open && d->m_dockPannel) {
        raise();
    }
}

QSize DockWidget::minimumSizeHint() const
{
    if (d->m_minimumSizeHintMode == DockWidget::MinimumSizeHintFromDockWidget || !d->m_widget) {
        return QSize(60, 40);
    } else {
        return d->m_widget->minimumSizeHint();
    }
}

/**
 * Only used when the feature flag DeleteContentOnClose is set.
 * Using the flag and setting a widget factory allows to free the resources
 * of the widget of your application while retaining the position the next
 * time you want to show your widget, unlike the flag DockWidgetDeleteOnClose
 * which deletes the dock widget itself. Since we keep the dock widget, all
 * regular features of ADS should work as normal, including saving and
 * restoring the state of the docking system and using perspectives.
 */
void DockWidget::setWidgetFactory(FactoryFunc createWidget, InsertMode insertMode)
{
    if (d->m_factory) {
        delete d->m_factory;
    }
    d->m_factory = new DockWidgetPrivate::WidgetFactory { createWidget, insertMode };
}

void DockWidget::showFullScreen()
{
    if (isFloating()) {
        dockContainer()->floatingWidget()->showFullScreen();
    } else {
        QWidget::showFullScreen();
    }
}

void DockWidget::showNormal()
{
    if (isFloating()) {
        dockContainer()->floatingWidget()->showNormal();
    } else {
        QWidget::showNormal();
    }
}

void DockWidget::raise()
{
    if (isClosed()) {
        return;
    }

    setAsCurrentTab();

    if (isInFloatingContainer()) {
        auto floatingWindow = window();
        floatingWindow->raise();
        floatingWindow->activateWindow();
    }
}

void DockWidget::deleteDockWidget()
{
    auto manager = dockManager();
    if (manager) {
        manager->removeDockWidget(this);
    }
    deleteLater();
    d->m_closed = true;
}

void DockWidget::closeDockWidget()
{
    closeDockWidgetInternal(true);
}

void DockWidget::setDockManager(DockManager *dockManager)
{
    d->m_dockManager = dockManager;
}

void DockWidget::setDockPannel(DockPannel *pannel)
{
    d->m_dockPannel = pannel;
    d->m_tab->setDockPannel(pannel);
    d->m_toggleViewAction->setChecked(pannel != nullptr && !this->isClosed());
    setParent(pannel);
}

void DockWidget::setToggleViewActionChecked(bool checked)
{
    QAction *action = d->m_toggleViewAction;
    //action->blockSignals(true);
    action->setChecked(checked);
    //action->blockSignals(false);
}

void DockWidget::saveState(QXmlStreamWriter &stream) const
{
    stream.writeStartElement("widget");
    stream.writeAttribute("name", objectName());
    stream.writeAttribute("closed", QVariant::fromValue(d->m_closed).toString());
    stream.writeEndElement();
}

void DockWidget::setClosedState(bool closed)
{
    d->m_closed = closed;
}

void DockWidget::flagAsUnassigned()
{
    d->m_closed = true;
    setParent(d->m_dockManager);
    setVisible(false);
    setDockPannel(nullptr);
    tab()->setParent(this);
}

void DockWidget::emitTopLevelEventForWidget(DockWidget *topLevelDockWidget, bool floating)
{
    if (topLevelDockWidget) {
        topLevelDockWidget->dockPannel()->updateTitleBarVisibility();
        topLevelDockWidget->emitTopLevelChanged(floating);
    }
}

void DockWidget::emitTopLevelChanged(bool floating)
{
    if (floating != d->m_isFloatingTopLevel) {
        d->m_isFloatingTopLevel = floating;
        emit topLevelChanged(d->m_isFloatingTopLevel);
        d->setToolBarFloatingStyle(floating);
    }
}

void DockWidget::toggleViewInternal(bool open)
{
    const DockContainer *const beforeDockContainerWidget = dockContainer();
    DockWidget *topLevelDockWidgetBefore = beforeDockContainerWidget
            ? beforeDockContainerWidget->topLevelDockWidget()
            : nullptr;

    if (open) {
        d->showDockWidget();
    } else {
        d->hideDockWidget();
    }

    d->m_closed = !open;
    //d->m_toggleViewAction->blockSignals(true);
    d->m_toggleViewAction->setChecked(open);

    //d->m_toggleViewAction->blockSignals(false);
    if (d->m_dockPannel) {
        d->m_dockPannel->toggleDockWidgetView(this, open);
    }

    if (open && topLevelDockWidgetBefore) {
        DockWidget::emitTopLevelEventForWidget(topLevelDockWidgetBefore, false);
    }

    // Here we need to call the dockContainer() function again, because if
    // this dock widget was unassigned before the call to showDockWidget() then
    // it has a dock container now
    const DockContainer *const dockContainerWidget = dockContainer();
    DockWidget *topLevelDockWidgetAfter = dockContainerWidget ? dockContainerWidget->topLevelDockWidget()
                                                              : nullptr;
    DockWidget::emitTopLevelEventForWidget(topLevelDockWidgetAfter, true);
    DockFloatingWidget *floatingContainer = dockContainerWidget ? dockContainerWidget->floatingWidget()
                                                                : nullptr;
    if (floatingContainer) {
        floatingContainer->updateWindowTitle();
    }

    if (!open) {
        emit closed();
    }

    emit viewToggled(open);
}

bool DockWidget::closeDockWidgetInternal(bool forceClose)
{
    if (!forceClose) {
        emit closeRequested();
    }

    if (!forceClose && features().testFlag(DockWidget::CustomCloseHandling)) {
        return false;
    }

    if (features().testFlag(DockWidget::DockWidgetDeleteOnClose)) {
        // If the dock widget is floating, then we check if we also need to
        // delete the floating widget
        if (isFloating()) {
            DockFloatingWidget *floatingWidget = internal::findParent<DockFloatingWidget *>(this);
            if (floatingWidget->dockWidgets().count() == 1) {
                floatingWidget->deleteLater();
            } else {
                floatingWidget->hide();
            }
        }
        deleteDockWidget();
        emit closed();
    } else {
        toggleView(false);
    }

    return true;
}

bool DockWidget::event(QEvent *event)
{
    switch (event->type()) {
    case QEvent::Hide:
        emit visibilityChanged(false);
        break;
    case QEvent::Show:
        emit visibilityChanged(geometry().right() >= 0 && geometry().bottom() >= 0);
        break;
    case QEvent::WindowTitleChange: {
        const auto title = windowTitle();
        if (d->m_tab) {
            d->m_tab->setText(title);
        }
        if (d->m_toggleViewAction) {
            d->m_toggleViewAction->setText(title);
        }
        if (d->m_dockPannel) {
            d->m_dockPannel->markTitleBarMenuOutdated(); // update tabs menu
        }
        auto floatingWidget = dockFloatingWidget();
        if (floatingWidget) {
            floatingWidget->updateWindowTitle();
        }
        emit titleChanged(title);
    } break;
    default:
        break;
    }

    return QWidget::event(event);
}

QADS_END_NAMESPACE
